package com.kafeblog.roo.portletmvc;
import static com.kafeblog.roo.portletmvc.PortletJavaType.RENDER_MAPPING;
import static org.springframework.roo.model.SpringJavaType.CONTROLLER;
import static org.springframework.roo.model.SpringJavaType.MODEL;
import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.PhysicalTypeCategory;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.TypeManagementService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.StringAttributeValue;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.JdkJavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.process.manager.MutableFile;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.DependencyScope;
import org.springframework.roo.project.DependencyType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.ProjectType;
import org.springframework.roo.project.Property;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.WebXmlUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.kafeblog.roo.portletmvc.utils.ValidateHelper;

/**
 * Implementation of {@link PortletmvcOperations} interface.
 *
 * @since 1.1.1
 */
@Component
@Service
public class PortletmvcOperationsImpl implements PortletmvcOperations {
	private static final String WEB_XML = "WEB-INF/web.xml";
	private static final String PORTLET_XML = "WEB-INF/portlet.xml";
	private static final String APPLICATION_CONTEXT_XML = "applicationContext.xml";
	private static final String PORTLET_CONTEXT_XML = "portlet-context.xml";
	private static final String PORTLET_API_VERSION = "1.0"; 
    private static final JavaSymbolName VALUE = new JavaSymbolName("value");
    
    @Reference protected FileManager fileManager;    
	@Reference protected PathResolver pathResolver;
	@Reference protected ValidateHelper validateHelper;
    @Reference protected ProjectOperations projectOperations;
    @Reference protected TypeManagementService typeManagementService;
	@Reference protected TypeLocationService typeLocationService;
	private Logger log = HandlerUtils.getLogger(getClass());

    
	@Override
	public boolean isSetupCommandAvailable() {
		// Always allow portlet setup
		log.info("isSetupCommandAvailable");
    	return true;
	}

	@Override
	public void performSetupCommand() {
		copyPortletXml();
		copyWebXml();
		updateRootContextXml();
		copyPortletContextXml();
		updateMavenDependencies();
	}
	
	
	


	@Override
	public boolean isAddCommandAvailable() {
		return isPortletInstalled();
	}

	@Override
	public void performAddCommand(JavaType className, String supportModes,
			String displayName, String category) {
		generatePortletController(className);
		generateViews(className, "index");
		updatePortletXml(className, supportModes);
	}
	
	
	




	@Override
	public boolean isPortletInstalled() {
		return validateHelper.filesMustExists(Path.SRC_MAIN_WEBAPP, WEB_XML, PORTLET_XML);
	}
	
	private void updateMavenDependencies() {
		final String moduleName = projectOperations.getFocusedModuleName();
		
		projectOperations.addProperty(moduleName, new Property("portlet.version", PORTLET_API_VERSION));
		
		List<Dependency> dependencies = new ArrayList<Dependency>();
		dependencies.add(new Dependency("org.springframework", "spring-webmvc-portlet", "${spring.version}"));
		dependencies.add(new Dependency("javax.portlet", "portlet-api", "${portlet.version}", DependencyType.JAR, DependencyScope.PROVIDED));
		projectOperations.addDependencies(moduleName, dependencies);
		projectOperations.updateProjectType(moduleName, ProjectType.WAR);
	}
	private void copyWebXml() {
		Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
                "Project metadata required");

        // Verify the servlet application context already exists
        final String portletCtxFilename = PORTLET_XML;
        Validate.isTrue(fileManager.exists(pathResolver.getFocusedIdentifier(
                Path.SRC_MAIN_WEBAPP, portletCtxFilename)),
                "'%s' does not exist", portletCtxFilename);

        final String webXmlPath = pathResolver.getFocusedIdentifier(
                Path.SRC_MAIN_WEBAPP, WEB_XML);
        if (fileManager.exists(webXmlPath)) {
            // File exists, so nothing to do
            return;
        }

        final InputStream templateInputStream = FileUtils.getInputStream(
                getClass(), "web-template.xml");
        Validate.notNull(templateInputStream,
                "Could not acquire web.xml template");
        final Document document = XmlUtils.readXml(templateInputStream);

        final String projectName = projectOperations
                .getProjectName(projectOperations.getFocusedModuleName());
        WebXmlUtils.setDisplayName(projectName, document, null);
        WebXmlUtils.setDescription("Roo generated " + projectName
                + " application", document, null);

        fileManager.createOrUpdateTextFileIfRequired(webXmlPath,
                XmlUtils.nodeToString(document), true);
	}
	
	private void updateRootContextXml() {
		// Add component scan if not exists
		// Add viewResolver if not exists
		final String xmlPath = pathResolver.getFocusedIdentifier(
                Path.SPRING_CONFIG_ROOT, APPLICATION_CONTEXT_XML);
		
		try {
			final InputStream contextStream = new FileInputStream(new File(xmlPath));
			
			final Document document = XmlUtils.readXml(contextStream);
			
			addImportPortletContext(document);
			
			fileManager.createOrUpdateTextFileIfRequired(xmlPath,
	                XmlUtils.nodeToString(document), true);
		} catch (FileNotFoundException e) {
			log.info("ApplicationContext not found");
		}
		
	}
	
	private void addImportPortletContext(final Document document) {
		Element root = (Element) document.getFirstChild();
		// TODO: Query by XPath instead, so no need to loop over importEls
        final List<Element> importEls = DomUtils.getChildElementsByTagName(root, "import");
        boolean existed = false;
        for (Element el : importEls) {
        	if (el.getAttribute("resource").equals("portlet-context.xml")) {
        		existed = true;
        		break;
        	}
        }
        
        if (existed) {
        	// Already import portlet-context.xml
        	return;
        }
        
        Element importEl = DomUtils.createChildElement("import", root, document);
        importEl.setAttribute("resource", "portlet-context.xml");
        root.appendChild(importEl);
    }	
	
	private void copyPortletXml() {
		Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
                "Project metadata required");

        final String xmlPath = pathResolver.getFocusedIdentifier(
                Path.SRC_MAIN_WEBAPP, PORTLET_XML);
        
        copyXmlFile("portlet-template.xml", xmlPath, true);
	}
	
	private void copyPortletContextXml() {
		Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
                "Project metadata required");

        // Verify the servlet application context already exists
        final String xmlPath = pathResolver.getFocusedIdentifier(
                Path.SPRING_CONFIG_ROOT, PORTLET_CONTEXT_XML);
        if (fileManager.exists(xmlPath)) {
            // File exists, so nothing to do
            return;
        }

        final InputStream templateInputStream = FileUtils.getInputStream(
                getClass(), "portlet-context.xml");
        Validate.notNull(templateInputStream,
                "Could not acquire portlet-context.xml template");
        final Document document = XmlUtils.readXml(templateInputStream);
        
        setBasePackageForComponentScan(document);

        fileManager.createOrUpdateTextFileIfRequired(xmlPath,
                XmlUtils.nodeToString(document), true);
	}
	
	private void setBasePackageForComponentScan(final Document document) {
        final Element componentScanElement = DomUtils.findFirstElementByName(
                "context:component-scan", (Element) document.getFirstChild());
        final JavaPackage topLevelPackage = projectOperations
                .getTopLevelPackage(projectOperations.getFocusedModuleName());
        componentScanElement.setAttribute("base-package",
                topLevelPackage.getFullyQualifiedPackageName());
    }
	
	private void copyXmlFile(final String src, final String dest, boolean override) {
		log.info("Copy " +src+ "  to " + dest);
        if (!override && fileManager.exists(dest)) {
            // File exists & not override, so nothing to do
            return;
        }

        final InputStream templateInputStream = FileUtils.getInputStream(
                getClass(), src);
        Validate.notNull(templateInputStream,
                "Could not acquire '"+dest+ "' template");
        final Document document = XmlUtils.readXml(templateInputStream);
        Validate.notNull(document, "Could not load xml document from stream");
        Validate.notNull(fileManager, "No fileManager");
        try {
        	
        fileManager.createOrUpdateTextFileIfRequired(dest,
                XmlUtils.nodeToString(document), true);
        } catch (Exception ex) {
        	ex.printStackTrace();
        }
	}

	private void generatePortletController(JavaType controller) {
		Validate.notNull(controller, "Portlet name required");
		Validate.isTrue(
                !JdkJavaType.isPartOfJavaLang(controller.getSimpleTypeName()),
                "Portlet name '%s' must not be part of java.lang",
                controller.getSimpleTypeName());
		
		final LogicalPath controllerPath = pathResolver
                .getFocusedPath(Path.SRC_MAIN_JAVA);
        final String resourceIdentifier = typeLocationService
                .getPhysicalTypeCanonicalPath(controller, controllerPath);
        final String declaredByMetadataId = PhysicalTypeIdentifier
                .createIdentifier(controller,
                        pathResolver.getPath(resourceIdentifier));

        // Create annotation @RequestMapping("VIEW")
        final List<AnnotationAttributeValue<?>> requestMappingAttributes = new ArrayList<AnnotationAttributeValue<?>>();
        requestMappingAttributes.add(new StringAttributeValue(VALUE, "VIEW"));
        
        List<AnnotationMetadataBuilder> annotations = null;
        annotations = new ArrayList<AnnotationMetadataBuilder>();
        annotations.add(new AnnotationMetadataBuilder(REQUEST_MAPPING,
                requestMappingAttributes));

        // Create annotation @Controller
        final List<AnnotationAttributeValue<?>> controllerAttributes = new ArrayList<AnnotationAttributeValue<?>>();
        annotations.add(new AnnotationMetadataBuilder(CONTROLLER,
                controllerAttributes));

        
        final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder(
                declaredByMetadataId, Modifier.PUBLIC, controller,
                PhysicalTypeCategory.CLASS);
        
		
		cidBuilder.setAnnotations(annotations);
		
		InvocableMemberBodyBuilder bodyBuilder = InvocableMemberBodyBuilder.getInstance();
		bodyBuilder.append("return \"index\"; // Return the view name");
		MethodMetadataBuilder renderMethodBuilder = new MethodMetadataBuilder(declaredByMetadataId, Modifier.PUBLIC, new JavaSymbolName("index"), JavaType.STRING, bodyBuilder);
		renderMethodBuilder.addParameter("locale", new JavaType(Locale.class));
		renderMethodBuilder.addParameter("model", MODEL);
		renderMethodBuilder.setAnnotations(Arrays.asList((new AnnotationMetadataBuilder(RENDER_MAPPING))));
		cidBuilder.addMethod(renderMethodBuilder.build());
		
		
		
		typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build());
	}
	
	private void generateViews(JavaType controller, String viewName) {
		String className = controller.getSimpleTypeName();
		if (!className.endsWith("Controller")) {
			return; // Not follow convention
		}
		
		String viewDir = className.substring(0, className.length() - "Controller".length()).toLowerCase();
		
		String viewFile = pathResolver.getFocusedIdentifier(
                Path.SRC_MAIN_WEBAPP, "WEB-INF/views/" + viewDir + "/" + viewName + ".jsp");
		if (fileManager.exists(viewFile)) { 
			// View existed, Ignored
			return;
		}
		MutableFile file = fileManager.createFile(viewFile);
		
		String viewTemplate = "view.jsp";
		InputStream viewTemplateStream = FileUtils.getInputStream(getClass(), viewTemplate);
		OutputStream viewStream = file.getOutputStream();
		try {
			IOUtils.copy(viewTemplateStream, viewStream);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			IOUtils.closeQuietly(viewTemplateStream);
			IOUtils.closeQuietly(viewStream);
		}		
	}
	private void updatePortletXml(JavaType className, String supportModes) {
		// Generate <portlet-name>-portlet.xml
		// Update portlet.xml for <portlet> tag: /portlet-app/portlet/portlet-name[className]
	}
}